<template>
  <div class="viewport" ref="viewport" @scroll="scrollFn">
    <div class="scroll-bar" ref="scrollBar"></div>
    <div class="scroll-list" :style="{transform:`translate3d(0,${offset}px,0)`}">
      <div v-for="item in visibleData" :key="item.id" :vid="item.id" ref="items">
        <slot :item="item"></slot>
      </div>
    </div>
  </div>
</template>

<script>
import throttle from 'lodash/throttle';

export default {
  name: "virtual-list",
  props: {
    size: Number, //当前每一项的高度
    remain: Number,  //可见多少个
    items: Array,
    variable: Boolean
  },
  mounted() {
    this.$refs.viewport.style.height = this.size * this.remain + 'px';
    this.$refs.scrollBar.style.height = this.items.length * this.size + 'px';

    //如果加载完毕 需要缓存每一项的高度
    this.cacheList();
  },
  data() {
    return {
      start: 0,
      end: this.remain, // 默认显示
      offset: 0
    }
  },
  created() {
    this.scrollFn = throttle(this.handleScroll, 200, {leading:false /*第一次不触发节流*/});
  },

  computed: {
    // 前面预留几个 (比如用户指定remain为5, 我们还要在前面加prevCount个, 在后面加nextCount个
    prevCount() {
      return Math.min(this.start, this.remain);
    },
    // 后面预留几个
    nextCount() {
      return Math.min(this.remain, this.items.length - this.end); //剩下的不足this.remain个就用this.remain个
    },
    // 可见的数据有哪些
    visibleData() {
      return this.items.slice(
        this.start - this.prevCount,
        this.end + this.nextCount
      );
    }
  },

  //再重新计算滚动条的高度
  updated() {
    // 页面渲染完成后 需要根据当前展示的数据 更新缓存区内容
    this.$nextTick(() => {
      // 根据当前显示的 更新缓存中的 height bottom top, 最终更新滚动条的高度
      const nodes = this.$refs.items; //获取真实的节点

      if (!(nodes && nodes.length > 0)) return;

      nodes.forEach(node => {
        const {height} = node.getBoundingClientRect();
        const id = node.getAttribute('vid') - 0;

        const oldHeight = this.positions[id].height;
        const x = oldHeight - height;
        if(x){
          this.positions[id].height = height;
          this.positions[id].bottom = this.positions[id].bottom - x; //缓存里对应item更新为真实的高度

          //将后续的所有人 都要向后移动
          for (let i = id + 1; i < this.positions.length; i++) {
            this.positions[i].top = this.positions[i - 1].bottom;
            this.positions[i].bottom = this.positions[i].bottom - x;
          }
        }
      });
      this.$refs.scrollBar.style.height = this.positions[this.positions.length - 1].bottom + 'px';
    });
  },
  methods: {
    handleScroll() {
      const scrollTop = this.$refs.viewport.scrollTop;

      if(this.variable){
        // 如果有传递variable 说明要使用二分查找找到对应的记录
        this.start = this.getStartIndex(scrollTop);
        this.end = this.start + this.remain;
        this.offset = this.positions[this.start - this.prevCount] ? this.positions[this.start - this.prevCount].top : 0;

      }else {
        // 获取当前应该从第几个开始渲染
        this.start = Math.floor(scrollTop / this.size); //130 40 3.25 //向下取整 因为 .25这个只滚过去了部分 然后因为索引是从0开始 so刚好向下取整就可以了 （即从滚过去部分的那个开始显示）
        // 当前可渲染的区域
        this.end = this.start + this.remain;
        // 定位当前的可视区域, 让当前渲染的内容在当前的viewport的可视区
        this.offset = (this.start - this.prevCount) * this.size;
      }
    },

    //缓存当前项的高度和top、bottom
    cacheList() {
      this.positions = this.items.map((item,index) => ({
        height: this.size, //variable参数下的话 size就成了预估的高度 而不是实际值
        top: index * this.size,
        bottom: (index + 1) * this.size
      }));
    },

    getStartIndex(value){ //  查找当前滚动的需要找到的值
      let start = 0;
      let end = this.positions.length - 1;
      let temp = null;

      while (start <= end) {
        let middleIndex = parseInt((start + end) / 2);
        let middleValue = this.positions[middleIndex].bottom; //找到当前的中间的那个人的结尾点
        if(middleValue == value){ // 如果直接找到了 就返回当前的下一个人
          return middleIndex + 1;
        }else if(middleValue < value){ // 当前要查找的人 在右边
          start = middleIndex + 1;
        }else if(middleValue > value){ // 当前要查找的人 在左边
          if (temp == null || temp > middleIndex) {
            temp = middleIndex
          }
          end = middleIndex - 1;
        }
      }
      return temp;
    }
  }
}
</script>

<style lang="scss">
.viewport {
  overflow: auto;
  position: relative;
}

.scroll-list {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
}
</style>
